package coveranalyzer.handlers;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;

import coveranalyzer.handlers.ReachablityAnalysisResult.Reachablity;

public class CodeReachablityAnalyser {

	private SearchHelper searchHelper;

	private Map<IType, String> types;

	private Map<IMethod, String> methods;

	private ReachablityAnalysisResult result;

	private List<IJavaProject> jProjects;

	public CodeReachablityAnalyser() {
		searchHelper = SearchHelper.create();
		jProjects = getJavaProjects();
		types = getTypesUnderWorkspace(jProjects);
		System.out.println(String.format("Total %d types found", types.size()));
		methods = getMethodsUnderWorkspace(types.keySet());
		System.out.println(String.format("Total %d methods found", methods
				.size()));
		result = new ReachablityAnalysisResult(getInitialReachableTypes());
		result.reportInitialReachableTypes();
		System.out.println("Analyzer initialized.");
	}

	public void analysisReachability() {
		int counter = 0;
		for (IType type : types.keySet()) {
			System.out.println(String.format("Analyzing %s, %d/%d",
					getElemString(type), ++counter, types.size()));
			Set<String> resolvedElems = new TreeSet<String>();
			if (!elementReachability(type, resolvedElems)) {
				System.out.println("\tUn reachable.");
			}
		}
		counter = 0;
		for (IMethod method : methods.keySet()) {
			System.out.println(String.format("Analyzing %s, %d/%d",
					getElemString(method), ++counter, types.size()));
			Set<String> resolvedElems = new TreeSet<String>();
			if (!elementReachability(method, resolvedElems)) {
				System.out.println("\tUn reachable.");
			}
		}
		result.reportUnReachableElements();
	}

	private boolean elementReachability(IJavaElement elem,
			Set<String> resolvedElems) {
		String elemStr = getElemString(elem);
		if (result.isReachable(elemStr) == Reachablity.YES)
			return true;
		if (result.isReachable(elemStr) == Reachablity.NO)
			return false;
		if (resolvedElems.contains(elemStr)) {
			return false;
		}
		resolvedElems.add(elemStr);

		boolean reachable = reachabilityByReferences(elem, resolvedElems);
		result.putResult(elemStr, reachable);
		return reachable;
	}

	private boolean reachabilityByReferences(IJavaElement elem,
			Set<String> resolvedElems) {
		Set<IJavaElement> referings = searchHelper.searchElementReference(elem);
		boolean reachable = false;
		for (IJavaElement refering : referings) {
			IJavaElement analysableUnit = Tools.resolveAnalysableUnit(refering);
			if (analysableUnit == null)
				continue;
			if (!isUnderWorkspace(analysableUnit)) {
				return true;
			}
			String auStr = getElemString(analysableUnit);
			if (result.isReachable(auStr) == Reachablity.NO) {
				continue;
			}
			if (result.isReachable(auStr) == Reachablity.YES) {
				return true;
			}
			if (elementReachability(analysableUnit, resolvedElems)) {
				return true;
			}
		}
		return reachable;
	}

	private boolean isUnderWorkspace(IJavaElement analysableUnit) {
		//FIXME Temporary class identify using specified package
		return analysableUnit.toString().startsWith("com.sybase.uep.xbw");
	}

	private String getElemString(IJavaElement elem) {
		if (elem instanceof IType) {
			if (!types.containsKey(elem)) {
				throw new RuntimeException(
						"CodeReachablityAnalyser#getElemString: encountered un-initialized type");
			}
			return types.get((IType) elem);
		} else if (elem instanceof IMethod) {
			if (!methods.containsKey(elem)) {
				System.out.println(elem);
				throw new RuntimeException(
						"CodeReachablityAnalyser#getElemString: encountered un-initialized mehtod");
			}
			return methods.get((IMethod) elem);
		} else {
			throw new RuntimeException(
					"CodeReachablityAnalyser#getElemString: invalid element type");
		}
	}

	private Map<IMethod, String> getMethodsUnderWorkspace(Set<IType> types) {
		Map<IMethod, String> rst = new LinkedHashMap<IMethod, String>();
		for (IType type : types) {
			IMethod[] methods2 = {};
			try {
				methods2 = type.getMethods();
			} catch (JavaModelException e) {
				e.printStackTrace();
			}
			for (IMethod method : methods2) {
				rst.put(method, Tools.IMethod2String(method));
			}
		}
		return rst;
	}

	private Map<IType, String> getTypesUnderWorkspace(
			List<IJavaProject> jProjects) {
		Map<IType, String> rst = new LinkedHashMap<IType, String>();
		for (IJavaProject proj : jProjects) {
			putAllTypes(rst, getTypesUnderProject(proj));
		}
		return rst;
	}

	private Map<IType, String> getTypesUnderProject(IJavaProject proj) {
		Map<IType, String> rst = new LinkedHashMap<IType, String>();
		try {
			IPackageFragment[] packages = proj.getPackageFragments();
			for (IPackageFragment mypackage : packages) {
				if (mypackage.getKind() != IPackageFragmentRoot.K_SOURCE)
					continue;
				putAllTypes(rst, getTypesUnderPackage(mypackage));
			}
		} catch (JavaModelException e) {
			e.printStackTrace();
		}
		return rst;
	}

	private Map<IType, String> getTypesUnderPackage(IPackageFragment mypackage) {
		Map<IType, String> rst = new LinkedHashMap<IType, String>();
		try {
			ICompilationUnit[] units = mypackage.getCompilationUnits();
			for (ICompilationUnit unit : units) {
				putAllTypes(rst, getTypesUnderCompilationUnit(unit));
			}
		} catch (JavaModelException e) {
			e.printStackTrace();
		}
		return rst;
	}

	private void putAllTypes(Map<IType, String> target,
			Map<IType, String> source) {
		for (Map.Entry<IType, String> entry : source.entrySet()) {
			target.put(entry.getKey(), entry.getValue());
		}
	}

	private Map<IType, String> getTypesUnderCompilationUnit(
			ICompilationUnit unit) {
		Map<IType, String> rst = new LinkedHashMap<IType, String>();
		try {
			IType[] types = unit.getTypes();
			for (IType type : types) {
				rst.put(type, Tools.IType2String(type));
				putAllTypes(rst, getTypesUnderType(type));
			}
		} catch (JavaModelException e) {
			e.printStackTrace();
		}
		return rst;
	}

	private Map<IType, String> getTypesUnderType(IType type) {
		Map<IType, String> rst = new LinkedHashMap<IType, String>();
		try {
			IType[] subTypes = type.getTypes();
			for (IType subType : subTypes) {
				rst.put(subType, Tools.IType2String(subType));
				putAllTypes(rst, getTypesUnderType(subType));
			}
		} catch (JavaModelException e) {
			e.printStackTrace();
		}
		return rst;
	}

	private Set<String> getInitialReachableTypes() {
		Set<String> types = new TreeSet<String>();
		for (IJavaProject proj : jProjects) {
			types.addAll(analyzeProjectRequiredTypes(proj));
		}
		return types;
	}

	private List<IJavaProject> getJavaProjects() {
		List<IJavaProject> rst = new ArrayList<IJavaProject>();
		IWorkspace workspace = ResourcesPlugin.getWorkspace();
		IWorkspaceRoot root = workspace.getRoot();
		IProject[] projects = root.getProjects();
		for (IProject project : projects) {
			if (!Tools.isJavaProject(project))
				continue;
			IJavaProject javaProject = JavaCore.create(project);
			rst.add(javaProject);
		}
		return rst;
	}

	private Set<String> analyzeProjectRequiredTypes(IJavaProject proj) {
		Set<String> rst = new TreeSet<String>();
		String projOSPath = proj.getResource().getLocation().toOSString();
		rst.addAll(analyzePluginFile(proj, projOSPath));
		rst.addAll(analyzeMFFile(proj, projOSPath));
		return rst;
	}

	private Set<String> analyzeMFFile(IJavaProject proj, String projOSPath) {
		String mfFilePath = String.format("%s\\META-INF\\MANIFEST.MF",
				projOSPath);
		String mfString = Tools.readFile(mfFilePath);
		Set<String> mfClasses = identifyClassFromConfig(proj, mfString);
		return mfClasses;
	}

	private Set<String> analyzePluginFile(IJavaProject proj, String projOSPath) {
		String pluginFilePath = String.format("%s\\plugin.xml", projOSPath);
		String pluginString = Tools.readFile(pluginFilePath).replaceAll(
				"(?s)<!--.*?-->", "");
		Set<String> pluginClasses = identifyClassFromConfig(proj, pluginString);
		return pluginClasses;
	}

	private Set<String> identifyClassFromConfig(IJavaProject proj, String config) {
		Set<String> rst = new TreeSet<String>();
		//FIXME Temporary class identify using specified package
		Pattern p = Pattern.compile("com\\.sybase\\.uep\\.xbw\\.[.\\w]*");
		Matcher m = p.matcher(config);
		while (m.find()) {
			try {
				IType type = proj.findType(m.group());
				if (type == null)
					continue;
				rst.add(types.get(type));
			} catch (JavaModelException e) {
				e.printStackTrace();
			}
		}
		return rst;
	}
}
